Оглавление

  • 1  Обзор и описание данных
  • 2  Предобработка и дополнение данных
    • 2.1  Обработка дубликатов
    • 2.2  Обработка пропущенных значений
    • 2.3  Дополнение данных
  • 3  Анализ данных
    • 3.1  Распределение по категориям
    • 3.2  Количество посадочных мест
    • 3.3  Сетевые и не сетевые заведения
      • 3.3.1  Распределение сетевых заведений по категориям
    • 3.4  Крупнейшие сети
    • 3.5  Районы, кварталы
    • 3.6  Пользовательский рейтинг
      • 3.6.1  Cетевые и не сетевые заведения
      • 3.6.2  Зависимость рейтинга от района
    • 3.7  Места на карте
    • 3.8  Популярные улицы
    • 3.9  Непопулярные улицы
    • 3.10  Средний чек
      • 3.10.1  Зависимость среднего чека от локации
      • 3.10.2  Зависимость среднего чека от типа заведения
    • 3.11  Выводы
  • 4  Открытие новой кофейни
    • 4.1  Выбор локации
    • 4.2  Доля кофеен от общего числа заведений
    • 4.3  Стоимость чашки кофе
    • 4.4  Категория цен
    • 4.5  Выбор часов работы
    • 4.6  Пользовательский рейтинг
    • 4.7  Выводы
  • 5  Презентация

Рынок заведений общественного питания Москвы¶

Исследование рынка заведений общественного питания г. Москва. Поиск интересных особенностей, которые в будущем помогут в выборе подходящего инвесторам места для открытия новых ресторанов.

Датасет с заведениями общественного питания Москвы, составлен на основе данных сервисов Яндекс Карты и Яндекс Бизнес (актуальность информации: лето 2022 года).

In [1]:
! pip install folium
Requirement already satisfied: folium in d:\anaconda\lib\site-packages (0.14.0)
Requirement already satisfied: numpy in d:\anaconda\lib\site-packages (from folium) (1.21.5)
Requirement already satisfied: jinja2>=2.9 in d:\anaconda\lib\site-packages (from folium) (2.11.3)
Requirement already satisfied: requests in d:\anaconda\lib\site-packages (from folium) (2.27.1)
Requirement already satisfied: branca>=0.6.0 in d:\anaconda\lib\site-packages (from folium) (0.6.0)
Requirement already satisfied: MarkupSafe>=0.23 in d:\anaconda\lib\site-packages (from jinja2>=2.9->folium) (2.0.1)
Requirement already satisfied: charset-normalizer~=2.0.0 in d:\anaconda\lib\site-packages (from requests->folium) (2.0.4)
Requirement already satisfied: certifi>=2017.4.17 in d:\anaconda\lib\site-packages (from requests->folium) (2021.10.8)
Requirement already satisfied: idna<4,>=2.5 in d:\anaconda\lib\site-packages (from requests->folium) (3.3)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in d:\anaconda\lib\site-packages (from requests->folium) (1.26.9)
In [2]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from plotly import graph_objects as go
import plotly.express as px
import folium
from folium import Marker, Map, Choropleth
from folium.plugins import MarkerCluster
from folium.features import CustomIcon
import urllib.request, json 

import warnings
warnings.filterwarnings('ignore')
In [3]:
sns.set_palette('pastel')
sns.set_style('whitegrid') 
palette = px.colors.qualitative.Pastel1
In [4]:
moscow_lat, moscow_lng = 55.751244, 37.618423

Обзор и описание данных¶

In [5]:
try:
    df = pd.read_csv('Datasets/moscow_places.csv')
except:
    df = pd.read_csv('https://code.s3.yandex.net/datasets/moscow_places.csv')
In [6]:
try:
    with open('C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson', 'r') as f:
        geo_json = json.load(f)
    state_geo = 'C:/Users/Александр/Jupiter Notebook/Yandex/8 - restaurants/Datasets/admin_level_geomap.geojson'
except:
    with urllib.request.urlopen("https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson") as url:
        geo_json = json.load(url)
    state_geo = 'https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson'   
In [7]:
df.head()
Out[7]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
0 WoWфли кафе Москва, улица Дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.878494 37.478860 5.0 NaN NaN NaN NaN 0 NaN
1 Четыре комнаты ресторан Москва, улица Дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.875801 37.484479 4.5 выше среднего Средний счёт:1500–1600 ₽ 1550.0 NaN 0 4.0
2 Хазри кафе Москва, Клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... 55.889146 37.525901 4.6 средние Средний счёт:от 1000 ₽ 1000.0 NaN 0 45.0
3 Dormouse Coffee Shop кофейня Москва, улица Маршала Федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.881608 37.488860 5.0 NaN Цена чашки капучино:155–185 ₽ NaN 170.0 0 NaN
4 Иль Марко пиццерия Москва, Правобережная улица, 1Б Северный административный округ ежедневно, 10:00–22:00 55.881166 37.449357 5.0 средние Средний счёт:400–600 ₽ 500.0 NaN 1 148.0

Описание наименований столбцов:

name — название заведения;
address — адрес заведения;
category — категория заведения, например «кафе», «пиццерия» или «кофейня»;
hours — информация о днях и часах работы;
lat — широта географической точки, в которой находится заведение;
lng — долгота географической точки, в которой находится заведение;
rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:

  • «Средний счёт: 1000–1500 ₽»;
  • «Цена чашки капучино: 130–220 ₽»;
  • «Цена бокала пива: 400–600 ₽».
  • и так далее;

middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»
middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»
chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым:

  • 0 — заведение не является сетевым
  • 1 — заведение является сетевым

district — административный район, в котором находится заведение, например Центральный административный округ;
seats — количество посадочных мест.

In [8]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8406 entries, 0 to 8405
Data columns (total 14 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   name               8406 non-null   object 
 1   category           8406 non-null   object 
 2   address            8406 non-null   object 
 3   district           8406 non-null   object 
 4   hours              7870 non-null   object 
 5   lat                8406 non-null   float64
 6   lng                8406 non-null   float64
 7   rating             8406 non-null   float64
 8   price              3315 non-null   object 
 9   avg_bill           3816 non-null   object 
 10  middle_avg_bill    3149 non-null   float64
 11  middle_coffee_cup  535 non-null    float64
 12  chain              8406 non-null   int64  
 13  seats              4795 non-null   float64
dtypes: float64(6), int64(1), object(7)
memory usage: 919.5+ KB

Некоторые столбцы содержат пропущенные значения. Также данные необходимо проверить на наличие дубликатов и некорректных значений.
В связи с этим проводится предобработка данных.

Предобработка и дополнение данных¶

Обработка дубликатов¶

In [9]:
df['category'].unique()
Out[9]:
array(['кафе', 'ресторан', 'кофейня', 'пиццерия', 'бар,паб',
       'быстрое питание', 'булочная', 'столовая'], dtype=object)
In [10]:
df['district'].unique()
Out[10]:
array(['Северный административный округ',
       'Северо-Восточный административный округ',
       'Северо-Западный административный округ',
       'Западный административный округ',
       'Центральный административный округ',
       'Восточный административный округ',
       'Юго-Восточный административный округ',
       'Южный административный округ',
       'Юго-Западный административный округ'], dtype=object)
In [11]:
df['price'].unique()
Out[11]:
array([nan, 'выше среднего', 'средние', 'высокие', 'низкие'], dtype=object)
In [12]:
df['chain'].unique()
Out[12]:
array([0, 1], dtype=int64)

Неявных дубликатов в столбцах с категориальными данными не обнаружено

Далее проводится предобработка столбцов с названием и адресом

In [13]:
to_change = ['name', 'address']
In [14]:
for col in to_change:
    for index in range(len(df[col])):
        df[col][index] = df[col][index].lower()
In [15]:
for index in range(len(df[col])):
    df['address'][index] = df['address'][index].replace('москва, ', '')
In [16]:
sum(df[['name', 'address']].duplicated())
Out[16]:
4

Найдено 4 заведения с одним названием и адресом, вероятно это одни и те же заведения, которые были записаны по-разному

In [17]:
df[df[['name', 'address']].duplicated(keep=False) == True]
Out[17]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
189 кафе кафе парк ангарские пруды Северный административный округ ежедневно, 09:00–23:00 55.880327 37.530786 3.2 NaN NaN NaN NaN 0 NaN
215 кафе кафе парк ангарские пруды Северный административный округ ежедневно, 10:00–22:00 55.881438 37.531848 3.2 NaN NaN NaN NaN 0 NaN
1430 more poke ресторан волоколамское шоссе, 11, стр. 2 Северный административный округ ежедневно, 09:00–21:00 55.806307 37.497566 4.2 NaN NaN NaN NaN 0 188.0
1511 more poke ресторан волоколамское шоссе, 11, стр. 2 Северный административный округ пн-чт 09:00–18:00; пт,сб 09:00–21:00; вс 09:00... 55.806307 37.497566 4.2 NaN NaN NaN NaN 1 188.0
2211 раковарня клешни и хвосты ресторан проспект мира, 118 Северо-Восточный административный округ ежедневно, 12:00–00:00 55.810553 37.638161 4.4 NaN NaN NaN NaN 0 150.0
2420 раковарня клешни и хвосты бар,паб проспект мира, 118 Северо-Восточный административный округ пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... 55.810677 37.638379 4.4 NaN NaN NaN NaN 1 150.0
3091 хлеб да выпечка булочная ярцевская улица, 19 Западный административный округ ежедневно, 09:00–22:00 55.738886 37.411648 4.1 NaN NaN NaN NaN 1 276.0
3109 хлеб да выпечка кафе ярцевская улица, 19 Западный административный округ NaN 55.738449 37.410937 4.1 NaN NaN NaN NaN 0 276.0

Так и есть, 4 заведения имеют по 2 записи.
Любопытно, что для некоторых из них значения category и chain отличается.
Необходимо выяснить, какие из записей оставить.

In [18]:
df[df['name'] == 'more poke']
Out[18]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
1430 more poke ресторан волоколамское шоссе, 11, стр. 2 Северный административный округ ежедневно, 09:00–21:00 55.806307 37.497566 4.2 NaN NaN NaN NaN 0 188.0
1511 more poke ресторан волоколамское шоссе, 11, стр. 2 Северный административный округ пн-чт 09:00–18:00; пт,сб 09:00–21:00; вс 09:00... 55.806307 37.497566 4.2 NaN NaN NaN NaN 1 188.0
6088 more poke ресторан духовской переулок, 19 Южный административный округ ежедневно, 10:00–22:00 55.704177 37.612889 4.4 NaN NaN NaN NaN 1 NaN
In [19]:
df[df['name'] == 'раковарня клешни и хвосты']
Out[19]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
2211 раковарня клешни и хвосты ресторан проспект мира, 118 Северо-Восточный административный округ ежедневно, 12:00–00:00 55.810553 37.638161 4.4 NaN NaN NaN NaN 0 150.0
2420 раковарня клешни и хвосты бар,паб проспект мира, 118 Северо-Восточный административный округ пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... 55.810677 37.638379 4.4 NaN NaN NaN NaN 1 150.0
7270 раковарня клешни и хвосты бар,паб братиславская улица, 12 Юго-Восточный административный округ пн-чт 12:00–00:00; пт,сб 12:00–01:00; вс 12:00... 55.659744 37.752984 4.9 средние Цена бокала пива:150–250 ₽ NaN NaN 1 40.0
In [20]:
df[df['name'] == 'хлеб да выпечка']
Out[20]:
name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats
3091 хлеб да выпечка булочная ярцевская улица, 19 Западный административный округ ежедневно, 09:00–22:00 55.738886 37.411648 4.1 NaN NaN NaN NaN 1 276.0
3109 хлеб да выпечка кафе ярцевская улица, 19 Западный административный округ NaN 55.738449 37.410937 4.1 NaN NaN NaN NaN 0 276.0
7937 хлеб да выпечка кофейня каширское шоссе, 61г Южный административный округ ежедневно, 09:00–22:00 55.621379 37.714108 4.5 NaN NaN NaN NaN 1 NaN

Заведения являются сетевыми, соответственно, необходимо оставить записи, где chain = 1

In [21]:
df = df.drop([189, 1430, 2211, 3109]).reset_index()

Данные приведены к единому виду, дубликаты удалены.

Далее обрабатываются пропуски:

Обработка пропущенных значений¶

In [22]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8402 entries, 0 to 8401
Data columns (total 15 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   index              8402 non-null   int64  
 1   name               8402 non-null   object 
 2   category           8402 non-null   object 
 3   address            8402 non-null   object 
 4   district           8402 non-null   object 
 5   hours              7867 non-null   object 
 6   lat                8402 non-null   float64
 7   lng                8402 non-null   float64
 8   rating             8402 non-null   float64
 9   price              3315 non-null   object 
 10  avg_bill           3816 non-null   object 
 11  middle_avg_bill    3149 non-null   float64
 12  middle_coffee_cup  535 non-null    float64
 13  chain              8402 non-null   int64  
 14  seats              4792 non-null   float64
dtypes: float64(6), int64(2), object(7)
memory usage: 984.7+ KB

Пропущенные значения столбца price могут быть заполнены на основании даныых middle_avg_bill и middle_coffee_cup

Определяем распределение среднего счета и средней цены чашки кофе:

In [23]:
for cat in df['price'].unique():
    median_bill = df.query('price == @cat')['middle_avg_bill'].quantile([0.25,0.5,0.75])
    print(cat)
    print(median_bill)
    print('--------------------')
nan
0.25   NaN
0.50   NaN
0.75   NaN
Name: middle_avg_bill, dtype: float64
--------------------
выше среднего
0.25    1250.0
0.50    1250.0
0.75    1500.0
Name: middle_avg_bill, dtype: float64
--------------------
средние
0.25    350.0
0.50    500.0
0.75    850.0
Name: middle_avg_bill, dtype: float64
--------------------
высокие
0.25    1750.0
0.50    2000.0
0.75    2500.0
Name: middle_avg_bill, dtype: float64
--------------------
низкие
0.25    150.0
0.50    180.0
0.75    250.0
Name: middle_avg_bill, dtype: float64
--------------------
In [24]:
for cat in df['price'].unique():
    median_coffee = df.query('price == @cat')['middle_coffee_cup'].quantile([0.25,0.5,0.75])
    print(cat)
    print(median_coffee)
    print('--------------------')
nan
0.25   NaN
0.50   NaN
0.75   NaN
Name: middle_coffee_cup, dtype: float64
--------------------
выше среднего
0.25    176.5
0.50    203.0
0.75    229.5
Name: middle_coffee_cup, dtype: float64
--------------------
средние
0.25    160.00
0.50    200.00
0.75    255.75
Name: middle_coffee_cup, dtype: float64
--------------------
высокие
0.25    250.0
0.50    250.0
0.75    250.0
Name: middle_coffee_cup, dtype: float64
--------------------
низкие
0.25    110.0
0.50    139.0
0.75    154.0
Name: middle_coffee_cup, dtype: float64
--------------------

На основании этих данных устанавливаем границы диапазонов для каждой ценовой категории:

In [25]:
avg_prices = pd.DataFrame({'Категория':df['price'].unique(),
                           'min_avg_bill':[None, 1001, 301, 1701, 0],
                           'max_avg_bill':[None, 1700, 1000, 999999, 300],
                           'min_avg_coffee':[None, 201, 156, 241, 0],
                           'max_avg_coffee':[None, 240, 200, 999999, 155]}
                         )
In [26]:
avg_prices
Out[26]:
Категория min_avg_bill max_avg_bill min_avg_coffee max_avg_coffee
0 NaN NaN NaN NaN NaN
1 выше среднего 1001.0 1700.0 201.0 240.0
2 средние 301.0 1000.0 156.0 200.0
3 высокие 1701.0 999999.0 241.0 999999.0
4 низкие 0.0 300.0 0.0 155.0

Далее заполняем датафрейм в соответствии с определенными выше границами:

In [27]:
low = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][4],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][4])]['price'].index)
med = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][2],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][2])]['price'].index)
med_high = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][1],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][1])]['price'].index)
high = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_avg_bill'] >= avg_prices['min_avg_bill'][3],
                             df['middle_avg_bill'] <= avg_prices['max_avg_bill'][3])]['price'].index)
In [28]:
for index in low:
    df['price'][index] = 'низкие'

for index in med:
    df['price'][index] = 'средние'

for index in med_high:
    df['price'][index] = 'выше среднего'
    
for index in high:
    df['price'][index] = 'высокие'
In [29]:
low_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][4],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][4])]['price'].index)
med_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][2],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][2])]['price'].index)
med_high_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][1],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][1])]['price'].index)
high_coffee = list(df[np.logical_and(df['price'].isnull() == True, 
                             df['middle_coffee_cup'] >= avg_prices['min_avg_coffee'][3],
                             df['middle_coffee_cup'] <= avg_prices['max_avg_coffee'][3])]['price'].index)
In [30]:
for index in low_coffee:
    df['price'][index] = 'низкие'

for index in med_coffee:
    df['price'][index] = 'средние'

for index in med_high_coffee:
    df['price'][index] = 'выше среднего'
    
for index in high_coffee:
    df['price'][index] = 'высокие'
In [31]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8402 entries, 0 to 8401
Data columns (total 15 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   index              8402 non-null   int64  
 1   name               8402 non-null   object 
 2   category           8402 non-null   object 
 3   address            8402 non-null   object 
 4   district           8402 non-null   object 
 5   hours              7867 non-null   object 
 6   lat                8402 non-null   float64
 7   lng                8402 non-null   float64
 8   rating             8402 non-null   float64
 9   price              4042 non-null   object 
 10  avg_bill           3816 non-null   object 
 11  middle_avg_bill    3149 non-null   float64
 12  middle_coffee_cup  535 non-null    float64
 13  chain              8402 non-null   int64  
 14  seats              4792 non-null   float64
dtypes: float64(6), int64(2), object(7)
memory usage: 984.7+ KB

В столбце price удалось заполнить около 700 значений.

Остальные значения заполнить не удается, поэтому оставляем пустыми.

Дополнение данных¶

Из столбца с адресом извлекается название улицы для последующего анализа и записывается в отдельный столбец.

In [32]:
df['street'] = df['address']
In [33]:
for index in range(len(df)):
    df['street'][index] = df['street'][index].split(',')[0].strip()

Также из информации о часах работы извлекаются данные о заведениях, работающих ежедневно и круглосуточно.

In [34]:
df['is_24/7'] = 0
In [35]:
for index in range(len(df['hours'])):
    if df['hours'][index] == 'ежедневно, круглосуточно':
        df['is_24/7'][index] = 1
In [36]:
df['is_24/7'].sum()
Out[36]:
730
In [37]:
df.head()
Out[37]:
index name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats street is_24/7
0 0 wowфли кафе улица дыбенко, 7/1 Северный административный округ ежедневно, 10:00–22:00 55.878494 37.478860 5.0 NaN NaN NaN NaN 0 NaN улица дыбенко 0
1 1 четыре комнаты ресторан улица дыбенко, 36, корп. 1 Северный административный округ ежедневно, 10:00–22:00 55.875801 37.484479 4.5 выше среднего Средний счёт:1500–1600 ₽ 1550.0 NaN 0 4.0 улица дыбенко 0
2 2 хазри кафе клязьминская улица, 15 Северный административный округ пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... 55.889146 37.525901 4.6 средние Средний счёт:от 1000 ₽ 1000.0 NaN 0 45.0 клязьминская улица 0
3 3 dormouse coffee shop кофейня улица маршала федоренко, 12 Северный административный округ ежедневно, 09:00–22:00 55.881608 37.488860 5.0 средние Цена чашки капучино:155–185 ₽ NaN 170.0 0 NaN улица маршала федоренко 0
4 4 иль марко пиццерия правобережная улица, 1б Северный административный округ ежедневно, 10:00–22:00 55.881166 37.449357 5.0 средние Средний счёт:400–600 ₽ 500.0 NaN 1 148.0 правобережная улица 0

Данные обработаны и дополнены, можно приступать к анализу.

Анализ данных¶

Распределение по категориям¶

In [38]:
category_pivot = df.pivot_table(index='category', 
                                values='name', 
                                aggfunc='count').sort_values(by='name', ascending=False)
In [39]:
category_pivot['percent'] = round(category_pivot['name'] / category_pivot['name'].sum() * 100, 2)
In [40]:
category_pivot
Out[40]:
name percent
category
кафе 2376 28.28
ресторан 2041 24.29
кофейня 1413 16.82
бар,паб 765 9.10
пиццерия 633 7.53
быстрое питание 603 7.18
столовая 315 3.75
булочная 256 3.05
In [41]:
plt.figure(figsize=(12, 5))

sns.barplot(x=category_pivot['percent'], 
            y=category_pivot.index
           )

plt.title('Количество заведений каждого типа (относительные величины)')
plt.ylabel('Категория')
plt.xlabel('Количество заведений (%)');

Из табличных и графических данных видно, что наибольшую долю составляют кафе и рестораны, также значительную часть заведений занимают кофейни.

Количество посадочных мест¶

In [42]:
plt.figure(figsize=(12, 5))

sns.boxplot(y='category', 
            x='seats', 
            data=df,
            order=df.groupby('category')['seats'].median().sort_values(ascending=False).index 
           ) 

plt.title('Зависимость количества мест от типа заведения')
plt.ylabel('Категория')
plt.xlabel('Количество мест');

На графике указано количество мест в заведении каждой категории.
Заметно, что бары и рестораны в среднем наиболее вместительные, а пекарни и пиццерии наоборот.
Также заметно, что во всех категориях присутствуют "выбросы" - заведения с большим количеством мест.

Сетевые и не сетевые заведения¶

Ниже представлено две диаграммы:

  1. Соотношение по количеству заведений
  2. Соотношение по количеству брендов
In [43]:
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'], 
                                      values=[df.query('chain == 1')['name'].count(),
                                              df.query('chain == 0')['name'].count()],
                                      marker_colors=palette,
                                      title = 'Соотношение количества сетевых и не сетевых заведений (по количеству точек)'
                                     )
                              ]
                        )
pie_category.show() 
In [44]:
pie_category = go.Figure(data=[go.Pie(labels=['сетевые', 'не сетевые'], 
                                      values=[df.query('chain == 1')['name'].nunique(),
                                              df.query('chain == 0')['name'].nunique()],
                                      marker_colors=palette,
                                      title = 'Соотношение количества сетевых и не сетевых заведений (по количеству наименований)'
                                     )
                              ]
                        )
pie_category.show() 

Как видно, сетей всего 13%, тем не менее, они занимают почти 40% рынка.

Распределение сетевых заведений по категориям¶

Для оценки данного распределения подсчитываются все заведения каждой категории, а также количество сетевых.
Далее находится соотношение (условно - вероятность того, что заведение определеннй категории является сетевым.)

In [45]:
category_pivot_chains = df.query('chain == 1').pivot_table(index='category', 
                                values='name', 
                                aggfunc='count').sort_values(by='name', ascending=False)
In [46]:
category_pivot = category_pivot.drop(['percent'], axis=1)
In [47]:
category_pivot_chains = category_pivot.merge(category_pivot_chains, on='category')
In [48]:
category_pivot_chains
Out[48]:
name_x name_y
category
кафе 2376 779
ресторан 2041 730
кофейня 1413 720
бар,паб 765 169
пиццерия 633 330
быстрое питание 603 232
столовая 315 88
булочная 256 157
In [49]:
category_pivot_chains.columns=('all_places', 'chain_places')
category_pivot_chains['chain_percent'] = round(category_pivot_chains['chain_places'] / \
                                               category_pivot_chains['all_places'], 2
                                              )
category_pivot_chains = category_pivot_chains.sort_values(by='chain_percent', 
                                                          ascending=False
                                                         )
In [50]:
category_pivot_chains
Out[50]:
all_places chain_places chain_percent
category
булочная 256 157 0.61
пиццерия 633 330 0.52
кофейня 1413 720 0.51
быстрое питание 603 232 0.38
ресторан 2041 730 0.36
кафе 2376 779 0.33
столовая 315 88 0.28
бар,паб 765 169 0.22
In [51]:
plt.figure(figsize=(12, 5))

sns.barplot(x=category_pivot_chains['chain_percent'], 
            y=category_pivot_chains.index
           )

plt.title('Зависимость количества сетевых заведений от типа (относительные величины)')
plt.ylabel('Категории')
plt.xlabel('Количество сетевых заведений');

Чаще всего булочные, пиццерии и кофейни являются сетевыми (небольшие заведения), в то время как бары и столовые чаще имеют только одну точку.

Крупнейшие сети¶

Отбираются самые крупные сети по количеству заведений.

In [52]:
top_15_chains = df.query('chain == 1').pivot_table(index ='name', 
                                                   values='chain',
                                                   aggfunc='count'
                                                  ).sort_values(by='chain', 
                                                                ascending=False
                                                               ).head(15)
top_15_chains.columns = ['places_total']
In [53]:
top_15_chains
Out[53]:
places_total
name
шоколадница 120
домино'с пицца 76
додо пицца 74
one price coffee 71
яндекс лавка 69
cofix 65
prime 50
хинкальная 44
кофепорт 42
кулинарная лавка братьев караваевых 39
теремок 38
чайхана 37
cofefest 32
буханка 32
му-му 27
In [54]:
plt.figure(figsize=(6, 10))

sns.barplot(x=top_15_chains['places_total'],
            y=top_15_chains.index.get_level_values(0)
             )

plt.title('Топ-15 сетей по количеству заведений')
plt.ylabel('Название сети')
plt.xlabel('Количество заведение');

Шоколадница - самая многочисленная сеть.
Также можно заметить, что в топе в основном кофейни.

Посмотрим количество сетей каждой категории:

In [55]:
top_15_chains_data = df.query('name in @top_15_chains.index')
In [56]:
plt.figure(figsize=(12, 5))

sns.countplot(y='category', 
            data=top_15_chains_data,
            order=top_15_chains_data.groupby('category')['name'].count().sort_values(ascending=False).index 
           ) 

plt.title('Количество заведений каждой категории среди Топ-15 сетей')
plt.ylabel('Категория')
plt.xlabel('Количество заведений');

Среди заведений Топ-15 сетей преобладают кофейни

In [57]:
plt.figure(figsize=(7, 15))

sns.countplot(y='name',
              data=top_15_chains_data,
              hue='category'
             )

plt.title('Типы заведений среди Топ-15 сетей')
plt.ylabel('Название сети')
plt.xlabel('Количество заведение');

Большинство из представленных сетей имеют определенный фокус - 5 сетей кофеен, 3 сети ресторанов, 2 сети пиццерий, 1 сеть кафе и 1 сеть булочных.
Также встречаются и разнонаправленные сети, такие как Му-Му

Районы, кварталы¶

In [58]:
plt.figure(figsize=(12, 5))

sns.countplot(y='district',
              data=df,
              order=df.groupby('district')['name'].count().sort_values(ascending=False).index
             )

plt.title('Распределение заведений по административным округам')
plt.ylabel('Административный округ')
plt.xlabel('Количество заведений');
In [59]:
district_df = df.groupby('district', as_index=False)['name'].agg('count')
district_df
Out[59]:
district name
0 Восточный административный округ 798
1 Западный административный округ 850
2 Северный административный округ 898
3 Северо-Восточный административный округ 890
4 Северо-Западный административный округ 409
5 Центральный административный округ 2242
6 Юго-Восточный административный округ 714
7 Юго-Западный административный округ 709
8 Южный административный округ 892
In [60]:
m_districts_total = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [61]:
Choropleth(
    geo_data=state_geo,
    data=district_df,
    columns=['district', 'name'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Количество заведений по районам',
).add_to(m_districts_total);
In [62]:
m_districts_total
Out[62]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Неудивительно, что в центе заведений общественного питания почти втрое больше.
Остальные районы, кроме Северо-Западного содержат примерно одинаковое количество точек.

Распределение по категориям внутри каждого района

In [63]:
plt.figure(figsize=(6, 12))

sns.countplot(y='district',
              data=df,
              hue='category',
              order=df.groupby('district')['name'].count().sort_values(ascending=False).index
             )

plt.title('Распределение заведений каждой категории по административным округам')
plt.ylabel('Административный округ, Категория')
plt.xlabel('Количество заведений');

Соотношение категорий похоже для всех районов.
Однако, в центральном заметно превалирование количества ресторанов, что объясняется престижностью локации и, следовательно, классом заведений.

Пользовательский рейтинг¶

Изучается зависимость рейтинга от различных факторов:

  • Тип заведения
  • Район
  • Принадлежность к сети
In [64]:
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])

sns.barplot(x='category',
            y='rating',
            data = df,
            order=df.groupby('category')['rating'].mean().sort_values(ascending=False).index
           )

plt.title('Средние рейтинги для каждой категории заведений')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);

Закономерно, точки быстрого питания имеют более низкие рейтинги, т.к. их фокус заключается в скорости обслуживания.
Бары и рестораны наоборот, стараются сделать посещение максимально комфортным, поэтому в среднем имеют более высокие рейтинги.

Cетевые и не сетевые заведения¶

In [65]:
plt.figure(figsize=(12, 5))
plt.axis([0,0,3.5,4.5])

sns.barplot(x = 'category',
            y='rating',
            data = df,
            hue='chain',
            order=df.groupby('category')['rating'].mean().sort_values(ascending=False).index
           )

plt.title('Средние рейтинги для каждой категории заведений (сравнение сетевых и не сетевых)')
plt.xlabel('Категория')
plt.ylabel('Средний рейтинг')
plt.xticks(rotation=90);

Интересное наблюдение - среди кафе пользователи предпочитают сетевые заведения, в то время как при посещении ресторанов или кофеен отдают предпочтение локальным точкам.

Зависимость рейтинга от района¶

In [66]:
rating_df = df.groupby('district', as_index=False)['rating'].agg('mean').round(2)
rating_df
Out[66]:
district rating
0 Восточный административный округ 4.17
1 Западный административный округ 4.18
2 Северный административный округ 4.24
3 Северо-Восточный административный округ 4.15
4 Северо-Западный административный округ 4.21
5 Центральный административный округ 4.38
6 Юго-Восточный административный округ 4.10
7 Юго-Западный административный округ 4.17
8 Южный административный округ 4.18
In [67]:
m_chor_rating = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [68]:
Choropleth(
    geo_data=state_geo,
    data=rating_df,
    columns=['district', 'rating'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Медианный рейтинг заведений по районам',
).add_to(m_chor_rating);
In [69]:
m_chor_rating
Out[69]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Средний рейтинг заведений в центре выше, однако разброс данных незначительный (всего 0,2 балла)

Места на карте¶

Все указанные в датасете заведения отмечены на карте ниже.
Для удобства навигации, каждая категория заведений имеет свою иконку.

In [70]:
m_clust = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [71]:
icons = {'кафе':'https://img.icons8.com/color/256/restaurant-building.png',
         'ресторан':'https://img.icons8.com/external-flaticons-flat-flat-icons/256/external-restaurant-vegan-and-vegetarian-flaticons-flat-flat-icons.png',
         'кофейня':'https://img.icons8.com/office/256/coffee.png',
         'пиццерия':'https://img.icons8.com/color/256/pizza.png',
         'бар,паб':'https://img.icons8.com/external-wanicon-lineal-color-wanicon/256/external-pub-st-patrick-day-wanicon-lineal-color-wanicon.png',
         'быстрое питание':'https://img.icons8.com/external-kosonicon-lineal-color-kosonicon/256/external-fast-food-back-to-school-kosonicon-lineal-color-kosonicon.png',
         'булочная':'https://img.icons8.com/color/256/bakery.png',
         'столовая':'https://img.icons8.com/color-glass/256/dining-room.png'
        }
In [72]:
marker_cluster = MarkerCluster().add_to(m_clust)
In [73]:
def create_clusters(row):
    for cat in df['category'].unique():
        if row['category'] == cat:
            icon_url = icons.get(cat)
            icon = CustomIcon(icon_url, icon_size=(30, 30))
        
            Marker([row['lat'], 
                    row['lng']],
                    popup=f"{row['name']} {row['rating']}",
                    icon=icon,
                  ).add_to(marker_cluster)
In [74]:
df.apply(create_clusters, axis=1);

m_clust
Out[74]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Популярные улицы¶

Исследуется, на каких улицах находися наибольшее количество заведений общественного питания

In [75]:
top_15_streets = df.pivot_table(index='street', 
                                values='name', 
                                aggfunc='count'
                               ).sort_values(by='name', 
                                             ascending=False
                                            ).head(15)
In [76]:
plt.figure(figsize=(12, 5))

sns.barplot(x=top_15_streets['name'], 
            y=top_15_streets.index
           )

plt.title('Топ-15 улиц по количеству заведений общественного питания')
plt.xlabel('Количество точек')
plt.ylabel('Улица');

Разумеется, в Топ-15 вошли крупные магистрали с большим потоком людей.

In [77]:
top_15_streets_data = df.query('street in @top_15_streets.index')

Распределение заведений по категориям

In [78]:
plt.figure(figsize=(6, 15))

sns.countplot(y='street',
              data=top_15_streets_data,
              hue='category',
              order=top_15_streets_data.groupby('street')['name'].count().sort_values(ascending=False).index
             )

plt.title('Распределений заведений по категориям на топ-15 улиц')
plt.ylabel('Улица')
plt.xlabel('Количество точек');

На некоторых улицах преобладают заведения определенной категории (например, кафе на МКАДе), в то время как на других распределение более равномерное.

Непопулярные улицы¶

Далее отбираются и изучаются улицы, на которых находится только одно заведение общественного питания

In [79]:
only_1_streets = df.pivot_table(index='street', 
                                values='name', 
                                aggfunc='count'
                               ).query('name == 1')
In [80]:
only_1_streets_data = df.query('street in @only_1_streets.index')
In [81]:
m_only_1 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [82]:
marker_cluster_2 = MarkerCluster().add_to(m_only_1)
In [83]:
def create_clusters_2(row):
    for cat in df['category'].unique():
        if row['category'] == cat:
            icon_url = icons.get(cat)
            icon = CustomIcon(icon_url, icon_size=(30, 30))
        
            Marker([row['lat'], 
                    row['lng']],
                    popup=f"{row['name']} {row['rating']}",
                    icon=icon,
                  ).add_to(marker_cluster_2)

Данные объекты наносяся на карту:

In [84]:
only_1_streets_data.apply(create_clusters_2, axis=1)

m_only_1
Out[84]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Как видно, распределение этих объектов идентично распределению всех заведений.
Улицы, на которых они располагаются непольшие, поэтому на них и было открыло не более 1го заведения.

In [85]:
only_1_category_pivot = only_1_streets_data.pivot_table(index='category', 
                                                        values='name', 
                                                        aggfunc='count'
                                                       ).sort_values(by='name', 
                                                                     ascending=False
                                                                    )
only_1_category_pivot.columns = ['places_total']
only_1_category_pivot['percent'] = round(only_1_category_pivot['places_total'] / \
                                         only_1_category_pivot['places_total'].sum() *100 , 2
                                        )
In [86]:
only_1_category_pivot
Out[86]:
places_total percent
category
кафе 159 34.79
ресторан 93 20.35
кофейня 84 18.38
бар,паб 39 8.53
столовая 36 7.88
быстрое питание 23 5.03
пиццерия 15 3.28
булочная 8 1.75
In [87]:
plt.figure(figsize=(12, 6))

sns.barplot(x=only_1_category_pivot['percent'], 
            y=only_1_category_pivot.index
           )

plt.title('Распределение заведений по категориям на улицах с 1-м заведением (Относительные величины)')
plt.ylabel('Категория')
plt.xlabel('% заведений');

Распределение по категориям среди данных улиц похоже на общее распределение, однако доля кафе на них выше.

Средний чек¶

Исследуется зависимость среднего чека от различных параметров:

  1. Район
  2. Тип заведения
  3. Принадлежность к сети

Зависимость среднего чека от локации¶

In [88]:
avg_bill_df = df.groupby('district', as_index=False)['middle_avg_bill'].agg('median')
avg_bill_df
Out[88]:
district middle_avg_bill
0 Восточный административный округ 575.0
1 Западный административный округ 1000.0
2 Северный административный округ 650.0
3 Северо-Восточный административный округ 500.0
4 Северо-Западный административный округ 700.0
5 Центральный административный округ 1000.0
6 Юго-Восточный административный округ 450.0
7 Юго-Западный административный округ 600.0
8 Южный административный округ 500.0

Из таблице видно, что средний чек заведений в центре и на западе выше, однако следует визуализировать данную информацию:

In [89]:
m_chor_bill = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [90]:
Choropleth(
    geo_data=state_geo,
    data=avg_bill_df,
    columns=['district', 'middle_avg_bill'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Медианный средний чек заведений по районам',
).add_to(m_chor_bill);
In [91]:
m_chor_bill
Out[91]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Отличие среднего чека в центре закономерно.
Высоки средний чек в Западном АО может объясняться вхождением в его состав Рублевского шоссе и аэропорта "Внуково"

Зависимость среднего чека от типа заведения¶

In [92]:
plt.figure(figsize=(7, 12))

sns.barplot(y='category',
            x='middle_avg_bill',
            data=df,
            order=df.groupby('category')['middle_avg_bill'].mean().sort_values(ascending=False).index
           )

plt.title('Средний чек для каждой категории')
plt.ylabel('Тип заведения')
plt.xlabel('Средний чек');

Высокий средний чек ресторанов и баров объясняется как высоким классом обслуживания, так и тем, что их посещают в компании.
Столовые и точки продажи фаст-фуда в основном ориентированы на индивидуальных посетителей и предлагают более низкие цены, соответственно ниже и средний чек.

In [93]:
plt.figure(figsize=(15, 7))

sns.barplot(x='category',
            y='middle_avg_bill',
            data=df,
            hue='chain',
            order=df.groupby('category')['middle_avg_bill'].mean().sort_values(ascending=False).index
           )

plt.title('Сравнение среднего чека сетевых и не сетевых заведений')
plt.xlabel('Тип заведения')
plt.ylabel('Средний чек');

Несетевые рестораны и бары в среднем дороже, вероятно это объясняется их уникальностью.
Среди кофеен и булочных наоборот, сети имеют возможность усатанвливать более высокие цены благодаря узнаваемости бренда.

Выводы¶

В наборе данных представлено множество заведений общественного уровня, разного типа, уровня цен, локаций и.т.д

Заведения распределены по локациям достаточно равномерно, их плотность увеличивается ближе к центру города. Наибольшее количество заведений имеют категории "Кафе" и "Ресторан", ввиду их универсальности и широкого меню:

  • Около 25% всех заведений расположена в центре (в основном там находятся рестораны, кафе, кофейни и бары) В центре количество более дорогих и эксклюзивных объектов выше, средняя стоимость блюд также повышается.

Также тип заведений в значительной мере зависит от того, где располагается объект.

Заведения в разных окрестных районах имеют схожие рейтинги

Также стоит отметить, что ценовая политика объектов различных категорий зависит от того, являются ли они сетевыми или индивидуальными.

Изложенные выше результаты в дальнейшем используются для составления бизнес-плана открытия новой кофейни.

Открытие новой кофейни¶

Перед открытием нового заведения необходимо изучить и определить:

  • наиболее подходящую локацию
  • ценовой диапазон предлагаемых напитков
  • предполагаемые часы работы

и другие ключевые параметры.

Выбор локации¶

In [94]:
coffee = df.query('category == "кофейня"')

coffee_count = coffee.groupby('district', as_index=False)['name'].agg('count')
coffee_count
Out[94]:
district name
0 Восточный административный округ 105
1 Западный административный округ 150
2 Северный административный округ 193
3 Северо-Восточный административный округ 159
4 Северо-Западный административный округ 62
5 Центральный административный округ 428
6 Юго-Восточный административный округ 89
7 Юго-Западный административный округ 96
8 Южный административный округ 131
In [95]:
coffee_count['name'].sum()
Out[95]:
1413

Сводная таблица с количеством кофеен в каждом районе.

Ниже приведена ее визуализация

In [96]:
m_chor_coffee = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
In [97]:
Choropleth(
    geo_data=state_geo,
    data=coffee_count,
    columns=['district', 'name'],
    key_on='feature.name',
    fill_color='YlGn',
    fill_opacity=0.8,
    legend_name='Количество кофеен по районам'
).add_to(m_chor_coffee);
In [98]:
m_chor_coffee
Out[98]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Количество кофеен в центре заметно выше, чем в других районах.

Для выбора наиболее подходящего места следует также изучить данные о проходимости.

Доля кофеен от общего числа заведений¶

In [99]:
district_df_coffee = district_df.merge(coffee_count, on='district')
In [100]:
district_df_coffee.columns=('district', 'total_places', 'coffee_places')
In [101]:
district_df_coffee['coffee_percent'] = round(district_df_coffee['coffee_places'] / \
                                            district_df_coffee['total_places'], 2)
In [102]:
district_df_coffee = district_df_coffee.sort_values(by='coffee_percent', ascending=False)
In [103]:
plt.figure(figsize=(12, 5))

sns.barplot(x=district_df_coffee['coffee_percent'], 
            y=district_df_coffee['district']
           )

plt.title('Доля кофеен от числа объектов')
plt.xlabel('Соотношение')
plt.ylabel('Район');

Стоимость чашки кофе¶

Стоит оценить среднюю стоимость чашки кофе имеющихся кофеен для определения целевой цены:

Поскольку данные могут содержать "выбросы", данные стоит предварительно отфильтровать

In [104]:
sns.boxplot(coffee['middle_coffee_cup']);

Посмотрим на заведения, в которых встречаются "выбросы" в значении стоимости чашки кофе

In [105]:
coffee[coffee['middle_coffee_cup'] > 400]
Out[105]:
index name category address district hours lat lng rating price avg_bill middle_avg_bill middle_coffee_cup chain seats street is_24/7
2856 2859 шоколадница кофейня большая семёновская улица, 27, корп. 1 Восточный административный округ ежедневно, 08:00–23:00 55.782268 37.709022 4.2 средние Цена чашки капучино:230–2907 ₽ NaN 1568.0 1 48.0 большая семёновская улица 0

Судя по всему, в столбце avg_bill допущена опечатка, из-за чего и столбец middle_coffe_cup записан неверно.
Следует исправить эти записи.

In [106]:
coffee['avg_bill'][2856] = 'Цена чашки капучино:230–290 ₽'

coffee['middle_coffee_cup'][2856] = 260
In [107]:
plt.figure(figsize=(12, 6))

sns.histplot(coffee['middle_coffee_cup'], kde=True)

plt.title('Распределение числа кофеен по стоимости чашки кофе')
plt.xlabel('Стоимость 1 чашки кофе, р.')
plt.ylabel('Количество кофеен')

plt.show();

Из графика можно заметить 3-4 пика.
Вероятно, они соответствуют ценовым категориям данных кофеен.
Следует изучить ценовые категории, а также их расположение, что поможет в выборе локации.

Категория цен¶

In [108]:
price_category_pivot = coffee.pivot_table(index='price', 
                                values='name', 
                                aggfunc='count').sort_values(by='name', ascending=False)
In [109]:
price_category_pivot
Out[109]:
name
price
средние 443
низкие 220
высокие 57
выше среднего 38
In [110]:
pie_price_category = go.Figure(data=[go.Pie(labels=price_category_pivot.index, 
                                      values=price_category_pivot['name'],
                                      marker_colors=palette,
                                      title = 'Распределение кофеен по ценовым категориям'
                                     )
                              ]
                        )

pie_price_category.show() 

Кофеен с низкими и умеренными ценами заметно больше.
Далее следует изучить распределение кофеен по районам и ценовым категориям.

In [111]:
plt.figure(figsize=(7, 12))

sns.countplot(y='district',
              data=coffee,
              hue='price',
              order=coffee.groupby('district')['category'].count().sort_values(ascending=False).index
             )

plt.title('Распределение кофеен по районам и ценовым категориям')
plt.ylabel('Район, категория')
plt.xlabel('Количество точек');

Исходя из полученной информации можно сделать вывод, что необходимо связывать выбираемую локацию и планируемум ценовую категорию кофейни.
В некоторый районах преобладают кофейни с низкими ценами, в других - со средними.
В определенных районах кофейни определнных ценовых категорий не представлены вовсе
(стоит также уточнить, это показатель пустующей ниши или отсутствия спроса).

Выбор часов работы¶

Исследуется количество кофеен, работающих ежедневно и круглосуточно.
Также изучается их распределение по районам.

In [112]:
coffee_24_7 = coffee[coffee['is_24/7'] == 1]
In [118]:
coffee_24_7_count = coffee_24_7.groupby('district', 
                                        as_index=False
                                       )['name'].agg('count').sort_values(by='name', 
                                                                          ascending=False
                                                                         )
coffee_24_7_count
Out[118]:
district name
5 Центральный административный округ 26
1 Западный административный округ 9
7 Юго-Западный административный округ 7
0 Восточный административный округ 5
2 Северный административный округ 5
3 Северо-Восточный административный округ 3
4 Северо-Западный административный округ 2
6 Юго-Восточный административный округ 1
8 Южный административный округ 1
In [114]:
coffee_24_7_count['name'].sum()
Out[114]:
59

Как видно из таблицы, кофеен с графиком 24/7 довольно мало.

Для выбора наиболее подходящих часов работы следует дополнительно изучить, связано ли это с отсутствием спроса
(вероятно, клиенты не заинтересованы в чашке кофе поздно вечером или ночью)

Пользовательский рейтинг¶

Исследуются значения рейтинга кофеен, расположенных в различных районах Москвы

In [115]:
coffee_rating = coffee.groupby('district', as_index=False)['rating'].agg('median')
coffee_rating
Out[115]:
district rating
0 Восточный административный округ 4.3
1 Западный административный округ 4.2
2 Северный административный округ 4.3
3 Северо-Восточный административный округ 4.3
4 Северо-Западный административный округ 4.3
5 Центральный административный округ 4.3
6 Юго-Восточный административный округ 4.3
7 Юго-Западный административный округ 4.3
8 Южный административный округ 4.3

Данные практически идентичны, средний рейтинг кофеен - 4,3 балла.

Выводы¶

  1. Всего в Москве расположено порядка 1400 кофеен (по состоянию на лето 2022г.)
  2. Треть из них расположена в центре.
  3. Примерно 1 из 25 кофеен работает в режиме 24/7
    • Это может быть связано в том числе с отсутствием спроса
  4. Медианный рейтинг кофеен 4,3 балла и не зависит от локации
  5. Подавляющее большинство кофеен имеют категорию цен "низкие" или "средние"
    • Это также может быть связано с отсутствием спроса на более дорогие кофейни
  6. Средняя стоимость чашки кофе - около 150р.

Презентация¶

Файл с презентацией